package lol.clann.minecraft.springboot.bukkit.utils;

import lol.clann.minecraft.springboot.bukkit.plugin.AdapterContext;
import org.slf4j.Logger;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 *
 * @author pyz
 * @date 2019/5/2 9:07 PM
 */
public class NMSReflectUtils {

    private static final Map<String, String> methodMapping = new HashMap<>();
    private static final Map<String, String> fieldMapping = new HashMap<>();
    private static final Map<String, String> classMapping = new HashMap<>();
    private static final Map<String, String> placeHolderMapping = new HashMap<>();
    private static final Pattern clPattern = Pattern.compile("CL: (.+)/([^/]+) (.+)");
    private static final Pattern fieldPattern = Pattern.compile("FD: (.+)/([^/]+)/([^/]+) (.+)/([^/]+)");
    private static final Pattern methodPattern = Pattern.compile("MD: (.+)/([^/]+)/([^/]+) (.+) (.*)/([^/]+) \\((.*)\\)(.*)");
    private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(NMSReflectUtils.class);

    static {
        placeHolderMapping.put("{nms}", "net.minecraft.server." + AdapterContext.getPackageVersion());
        placeHolderMapping.put("{nm}", "net.minecraft");
        placeHolderMapping.put("{cb}", "org.bukkit.craftbukkit." + AdapterContext.getPackageVersion());
        try {
            init();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static void init() throws IOException {
        URL srg = AdapterContext.getRemappingResource();
        if (srg == null) {
            return;
        }
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(srg.openStream(), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("CL: ")) {
                    Matcher m = clPattern.matcher(line);
                    if (!m.find()) {
//                        LOGGER.warn("fail to resolve " + line);
                        continue;
                    }
                    classMapping.put(m.group(1) + "/" + AdapterContext.getPackageVersion() + "/" + m.group(2), m.group(3).replace('/', '.'));
                } else if (line.startsWith("FD: ")) {
                    Matcher m = fieldPattern.matcher(line);
                    if (!m.find()) {
//                        LOGGER.warn("fail to resolve " + line);
                        continue;
                    }
                    fieldMapping.put(m.group(4).replace('/', '.') + "." + m.group(3), m.group(5));
                } else if (line.startsWith("MD: ")) {
                    Matcher m = methodPattern.matcher(line);
                    if (!m.find()) {
//                        LOGGER.warn("fail to resolve " + line);
                        continue;
                    }
                    methodMapping.put(m.group(5).replace('/', '.') + "." + m.group(3) + " (" + m.group(7) + ")", m.group(6));
                }
            }
        }
    }

    public static Class forName(String... classNames) throws ClassNotFoundException {
        for (String className : classNames) {
            boolean nms = className.contains("{nms}");
            for (Map.Entry<String, String> entry : placeHolderMapping.entrySet()) {
                className = className.replace(entry.getKey(), entry.getValue());
            }
            if (nms && AdapterContext.isRemapping()) {
                className = className.replace('.', '/');
                className = classMapping.getOrDefault(className, className);
                if (className.contains("/")) {
                    className = className.replace('/', '.');
                }
            }
            try {
                return Class.forName(className);
            } catch (ClassNotFoundException | NullPointerException e) {
                LOGGER.warn("fail to forName class:" + className + ". " + e.getClass().getSimpleName() + ":" + e.getMessage());
            }
        }
        throw new ClassNotFoundException(Arrays.toString(classNames));
    }

    public static Field getField(Class clazz, String... fieldNames) {
        for (String fieldName : fieldNames) {
            Field field = getField0(clazz, fieldName);
            if (field != null) {
                return field;
            }
        }
        throw new RuntimeException(String.format("can not find field  %s in class %s", Arrays.toString(fieldNames), clazz.getName()));
    }


    public static Method getMethod(Class clazz, Class[] parameterTypes, String... methodNames) {
        for (String methodName : methodNames) {
            try {
                Method method = getMethod0(clazz, parameterTypes, methodName);
                if (method != null) {
                    return method;
                }
            } catch (Throwable e) {
                // ignore
            }
        }
        throw new RuntimeException(String.format("can not find method  %s(%s) in class %s", Arrays.toString(methodNames), toSignature(parameterTypes), clazz.getName()));
    }

    private static Field getField0(Class clazz, String fieldName) {
        if (clazz == null || clazz == Object.class) {
            return null;
        }
        if (clazz.getName().startsWith("net.minecraft.")) {
            StringBuilder sb = new StringBuilder();
            sb.append(clazz.getName());
            sb.append(".").append(fieldName);
            String key = sb.toString();
            fieldName = fieldMapping.getOrDefault(key, fieldName);
        }
        try {
            return clazz.getDeclaredField(fieldName);
        } catch (NoSuchFieldException e) {
            return getField0(clazz.getSuperclass(), fieldName);
        }
    }

    private static Method getMethod0(Class clazz, Class[] parameterTypes, String methodName) {
        if (clazz == null) {
            return null;
        }
        if (clazz.getName().startsWith("net.minecraft.")) {
            String key = toKey(clazz, parameterTypes, methodName);
            methodName = methodMapping.getOrDefault(key, methodName);
        }
        try {
            return clazz.getDeclaredMethod(methodName, parameterTypes);
        } catch (NoSuchMethodException e) {
            return getMethod0(clazz.getSuperclass(), parameterTypes, methodName);
        }
    }

    private static String toKey(Method method) {
        return toKey(method.getDeclaringClass(), method.getParameterTypes(), method.getName());
    }

    private static String toKey(Class clazz, Class[] parameterTypes, String methodName) {
        StringBuilder sb = new StringBuilder();
        sb.append(clazz.getName());
        sb.append(".").append(methodName);
        sb.append(" ");
        sb.append("(");
        if (parameterTypes != null) {
            for (Class parameterType : parameterTypes) {
                sb.append(toSignature(parameterType));
            }
        }
        sb.append(")");
        String key = sb.toString();
        return key;
    }

    private static String toSignature(Class clazz) {
        return Array.newInstance(clazz, 0).getClass().getName().substring(1).replace('.', '/');
    }

    private static String toSignature(Class[] classes) {
        StringBuilder sb = new StringBuilder();
        for (Class parameterType : classes) {
            sb.append(toSignature(parameterType));
        }
        return sb.toString().replace('.', '/');
    }
}
